fix(spawn): keep strong ref to fire-and-forget subagent task - #13
Merged
zhao9797 merged 3 commits intoJul 29, 2026
Merged
Conversation
spawn_subagent(wait=False) called asyncio.create_task(_run_child()) without keeping a reference. asyncio only holds weak references to tasks, so the returned Task could be garbage-collected between the tool return and the child harness's first await point, silently dropping the subagent's result and leaving pending_subagents stuck. Store the task in a module-level _BACKGROUND_TASKS set and discard it via a done-callback — the canonical pattern documented in the asyncio create_task() docs. Add a regression test that pins a child task on an event, GC-collects, and asserts the task remains reachable.
zhao9797
approved these changes
Jul 29, 2026
zhao9797
left a comment
Contributor
There was a problem hiding this comment.
Reviewed the fire-and-forget subagent lifecycle path. Holding the created Task in a module-level set and discarding it on completion matches asyncio's documented pattern, does not affect the synchronous path, and cleans up on both success and failure. The spawn_subagent unit tests pass with the dev async test extra, and I also checked the failure path releases the strong reference.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
spawn_subagent(wait=False)callsasyncio.create_task(_run_child())without storing the returned Task (harnessx/tools/spawn_subagent.py:227). Because the asyncio event loop holds only weak references to tasks, an idle interval between the tool return and the child harness's firstawaitallows the task to be garbage-collected — silently dropping the subagent's result and leaving its entry stuck inparent_state.pending_subagents.For comparison, both
harnessx/processors/light_memory/processors.py:242andharnessx/tools/web_search.py:416already use the canonical "hold a set of tasks + discard on done" pattern; only this call site was missed.Change
_BACKGROUND_TASKS: set[asyncio.Task]alongside_spawn_ctx.spawn_subagent:<label>) and register adiscarddone-callback.Tests
New regression
test_spawn_async_task_is_referenced_until_done:asyncio.Eventso the task is definitely still running when we probe._BACKGROUND_TASKS.gc.collect()and re-asserts (proves the strong ref is holding).Full suite: 946 → 947 unit tests passing (same 1 pre-existing macOS unrelated failure).
References
Checklist
pytest tests/unit/ tests/integration/ -qpasses (947 passed, 1 pre-existing unrelated failure)